package com.blustreaksw.cyclecoach.service;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Locale;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONTokener;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.speech.tts.TextToSpeech;
import android.util.Log;

import com.blustreaksw.cyclecoach.MainActivity;
import com.blustreaksw.cyclecoach.R;
import com.blustreaksw.cyclecoach.btle.BtleDevice;
import com.blustreaksw.cyclecoach.btle.BtleDeviceStatus;
import com.blustreaksw.cyclecoach.btle.BtleDeviceType;
import com.blustreaksw.cyclecoach.data.WorkoutDatabaseHelper;

public class CycleCoachService extends Service {
	
	private static final int CYCLECOACH_SERVICE_ID = 2097152;
	private static final String TAG = "CycleCoachService";
	private WorkoutDatabaseHelper sWorkoutDb;
	private Handler mHandler;
	private boolean mServiceShutdownStarted;
	private boolean mInForeground;
	
	private TextToSpeech mTTS;
	
	
	private int mSessionTimeElapsed;
	private double mSessionDistanceTraveled;
	
	
	private void vocalizeString(String text) {
		if(mTTS != null && text != null) {
			if(mTTS.isSpeaking() == false) {
				mTTS.speak(text, TextToSpeech.QUEUE_FLUSH, null);
			}
		}
	}
	
	private void startSessionTimer() {
		resetSessionTimeDistance();
		Bundle bundle = new Bundle();
		bundle.putInt(EXTRA_TIME_ELAPSED, mSessionTimeElapsed);
		sendMsgToClient(MSG_DISPLAY_TIME_UPDATE, 0, 0, bundle);
		continueSessionTimer();
	}
	
	private void continueSessionTimer() {
		if(dbUpdatesActive()) {
			mHandler.postDelayed(new Runnable() {

				@Override
				public void run() {
					continueSessionTimer();
					mSessionTimeElapsed++;
					Bundle bundle = new Bundle();
					bundle.putInt(EXTRA_TIME_ELAPSED, mSessionTimeElapsed);
					sendMsgToClient(MSG_DISPLAY_TIME_UPDATE, 0, 0, bundle);
				}
				
			}, 1000);
			
		}
	}
	
	private void resetSessionTimeDistance() {
		mSessionTimeElapsed = 0;
		mSessionDistanceTraveled = 0.0;
	}
	
	////////////////////////////////////////////////////////////////////////
	//  Overridden Service object functionality.
	////////////////////////////////////////////////////////////////////////
	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return mMessenger.getBinder();
	}
	
	@Override
	public void onCreate() {
		sWorkoutDb = WorkoutDatabaseHelper.getInstance(getApplicationContext());
		mHandler = new Handler();
		mServiceShutdownStarted = false;
		mClientDevices = new ArrayList<BtleDevice>();
		
		mInForeground = false;
		
		mTTS = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
			
			@Override
			public void onInit(int status) {
				if(status == TextToSpeech.SUCCESS) {
					mTTS.setLanguage(Locale.getDefault());
				} else {
					mTTS = null;
					Log.d(TAG, "Failed to initialize TTS engine.");
				}
			}
		});
		
		// on start we need to check the saved state and recreate
		// any devices where we previously had active notifications
		// running
		
		/*try {
			mClientDevices = loadDevices();
		} catch (Exception e) {
			mClientDevices = new ArrayList<BtleDevice>();
			Log.e(TAG, "Error loading active devices");
		}
		
		if(!mClientDevices.isEmpty()) {
			// Walk the list and reconnect to every BtleDevice
			// TODO
			// Start notifications for all devices that were
			// marked as previously active.
		}*/
		
		Log.d(TAG, "CycleCoach service created");
		
		// Start a timer that checks for a client registration, and kill the service
		// if nothing is registered in that timeframe and notifications are inactive.
		Runnable timeout = new Runnable() {
            @Override
            public void run() {
            	if(!clientNotificationsActive()) {
	            	synchronized(mClientSyncKey) {
	        			if(mClient == null) shutdownServerProcess();
	        			}
            	}
            }
        };
        mHandler.postDelayed(timeout, 180000); // wait 180 seconds	
	}
	
	@Override
	public void onStart(Intent intent, int startId) {
		
	}
	
	@Override
	public void onDestroy() {
		
		/*if(!mClientDevices.isEmpty()) {
			if(!notificationsActive() && !mClientDevices.isEmpty()) {
				Log.e(TAG, "Severe error --- mClientDevices still populated but notifications disabled!");
				throw new RuntimeException();
			}
			
			try {
				saveDevices();
			} catch (JSONException e) {
				Log.e(TAG, "Failed to persist device list" + e);
			} catch (IOException e) {
				Log.e(TAG, "Failed to persist device list" + e);
			}
		}*/
		
		if(!mClientDevices.isEmpty()) {
			for(BtleDevice device : mClientDevices) {
				device.closeDeviceConnection();
			}
		}
		
		if(mTTS != null) {
			mTTS.stop();
			mTTS.shutdown();
		}
		
		Log.d(TAG, "CycleCoach service destroyed");
		super.onDestroy();
	}
	
	/***********************************************************************
     * Call removes user visible service notification, disconnects all
     * devices and makes call to stop service.
     ***********************************************************************/
	private void shutdownServerProcess() {
		synchronized(this) {
			if(mServiceShutdownStarted) { 
				return;
			} else {
				mServiceShutdownStarted = true;
			}
		}
		
		clearServiceNotification();
		
		// Cleanup all of our devices and then trigger shutdown of the process.
		disableClientNotifications();
		disconnectAllBtleDevices();
		stopSelf();
	}
	
	/********************************************************************************
	 * Walk the mClientDevices list and call closeDeviceConnection for each BtleDevice.
	 ********************************************************************************/
    private void disconnectAllBtleDevices() {
    	for(BtleDevice s : mClientDevices) {
    		s.closeDeviceConnection();
    		Log.d(TAG, "BtleDevice shutdown initiated for " + s.getBtleDeviceAddress());
    	}
    }
	
	/***********************************************************************
     * Shows a persistent notification that informs the user that the
     * service is active, and moves the process to the foreground to
     * prevent it from being killed by the O/S.
     ***********************************************************************/
	private void showServiceNotification() {
		
		if(mInForeground) return;
		
		
		mInForeground = true;
		
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                new Intent(this, MainActivity.class), 
                Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
		
		Notification notification = new Notification.Builder(this)
			.setContentTitle("Started CycleCoach service")
			.setSmallIcon(R.drawable.ic_notify)
			.setOngoing(true)
			.setContentIntent(contentIntent)
			.build();
		
		startForeground(CYCLECOACH_SERVICE_ID, notification);
	}
	
	/***********************************************************************
     * Clears the persistent notification and removes the application
     * from the foreground.
     ***********************************************************************/
	private void clearServiceNotification() {
		
		if(mInForeground) {
			mInForeground = false;
			stopForeground(true);
		}
	}
	
	/***********************************************************************
     * Client application handle.
     ***********************************************************************/
	private Messenger mClient;
	
	/********************************************************************************
     * Manages sending messages to the registered client.
     ********************************************************************************/
	private void sendMsgToClient(int command, int arg1, int arg2, Bundle bundle) {
		try {
			synchronized(mClientSyncKey) {
				if(mClient != null) {
					Message msg = Message.obtain(null, command, arg1, arg2);
					if(bundle != null) {
						msg.setData(bundle);
					}
					mClient.send(msg);
					return;
				}
			}
			
			// If we reach here then we didn't have a client to connect to.
			clientConnectionLost();
			
		} catch (RemoteException e) {
			clientConnectionLost();
		} 
	}
	
	/********************************************************************************
     * Handles case where we lost connection with the client.
     ********************************************************************************/
	private void clientConnectionLost() {
		synchronized(mClientSyncKey) {
			mClient = null;
		}
		
		disableClientNotifications();
		
		if(!sWorkoutDb.isWorkoutActive(getApplicationContext())) {
			shutdownServerProcess();
		}
	}
	
	////////////////////////////////////////////////////////////////////////
	//  Incoming and Outgoing Bundle key values
	////////////////////////////////////////////////////////////////////////
	/***********************************************************************
     * ###Outgoing###
     * Bundle key for transferring speed value from speed and cadence sensor.
     ***********************************************************************/
	public static final String EXTRA_CSC_SPEED =  
			CycleCoachService.class.getPackage().getName() + ".extra_csc_speed";
	
	/***********************************************************************
     * ###Outgoing###
     * Bundle key for transferring crank RPM value from speed and cadence
     * sensor.
     ***********************************************************************/
	public static final String EXTRA_CSC_CRANK_RPM =  
			CycleCoachService.class.getPackage().getName() + ".extra_csc_crank_rpm";
	
	/***********************************************************************
     * ###Outgoing###
     * Bundle key for transferring power output in watts from the power meter.
     ***********************************************************************/
	public static final String EXTRA_POWER_WATTS =  
			CycleCoachService.class.getPackage().getName() + ".extra_power_watts"; 
	
	/***********************************************************************
     * ###Outgoing###
     * Bundle key for transferring beats per minute value from heart rate
     * monitor.
     ***********************************************************************/
	public static final String EXTRA_HRM_BPM =  
			CycleCoachService.class.getPackage().getName() + ".extra_hrm_bpm";
	
	/***********************************************************************
     * ###Outgoing###
     * Bundle key for transferring timestamp value that denotes when data
     * received from any sensor.
     ***********************************************************************/
	public static final String EXTRA_TIMESTAMP =  
			CycleCoachService.class.getPackage().getName() + ".extra_timestamp";
	
	/***********************************************************************
     * Bundle key for ???
     ***********************************************************************/
	public static final String BTLE_SERVICE = 
			CycleCoachService.class.getPackage().getName() + ".btle_service";
	
	/***********************************************************************
	 * ###Incoming###
     * Bundle key for specifying the wheel circumference to use for
     * calculating speed with the CSC sensor.
     ***********************************************************************/
	public static final String EXTRA_WHEEL_CIRCUMFERENCE = 
			CycleCoachService.class.getPackage().getName() + ".extra_wheel_circumference";
	
	/***********************************************************************
	 * ###Incoming###
     * Bundle key for specifying session ID to supply to devices when
     * starting data update notification services.
     ***********************************************************************/
	public static final String EXTRA_SESSION_ID = 
			CycleCoachService.class.getPackage().getName() +".extra_session_id";
	
	/***********************************************************************
	 * ###Incoming###
     * Bundle key for specifying the Bluetooth HEX address to look for
     * when searching for LE devices to connect to.
     ***********************************************************************/
	public static final String EXTRA_BTLE_ADDRESS = 
			CycleCoachService.class.getPackage().getName() + "extra_btle_address";
	
	/***********************************************************************
	 * ###Incoming###
     * Bundle key for specifying the type of Bluetooth LE device we would
     * like to connect to.
     ***********************************************************************/
	public static final String EXTRA_BTLE_TYPE = 
			CycleCoachService.class.getPackage().getName() + ".extra_btle_type";
	
	/***********************************************************************
	 * ###Outgoing###
     * Bundle key for specifying the error experienced on the Bluetooth LE
     * device in user readable string.
     ***********************************************************************/
	public static final String EXTRA_BTLE_ERROR_STRING = 
			CycleCoachService.class.getPackage().getName() + ".extra_btle_error_string";
	
	/***********************************************************************
	 * ###Outgoing###
     * Bundle key for specifying the error experienced on the Bluetooth LE
     * device using integer value.
     ***********************************************************************/
	public static final String EXTRA_BTLE_ERROR_VALUE = 
			CycleCoachService.class.getPackage().getName() + ".extra_btle_error_value";
	
	/***********************************************************************
	 * ###Outgoing###
     * Bundle key for specifying the manufacturer defined name of the device.
     ***********************************************************************/
	public static final String EXTRA_BTLE_DEVICE_NAME = 
			CycleCoachService.class.getPackage().getName() + ".extra_btle_device_name";
	
	/***********************************************************************
	 * ###Outgoing###
     * Bundle key for 
     ***********************************************************************/
	public static final String EXTRA_TOTAL_DISTANCE = 
			CycleCoachService.class.getPackage().getName() + ".extra_total_distance";
	
	/***********************************************************************
	 * ###Outgoing###
     * Bundle key for 
     ***********************************************************************/
	public static final String EXTRA_TIME_ELAPSED = 
			CycleCoachService.class.getPackage().getName() + ".extra_time_elapsed";
	
	
	////////////////////////////////////////////////////////////////////////
	//  List of message types that can be sent from registered clients.
	////////////////////////////////////////////////////////////////////////
	/***********************************************************************
     * Sent when client would like to create a persistent connection with
     * the service.
     ***********************************************************************/
	public static final int MSG_REGISTER_CLIENT = 1;
	
	/***********************************************************************
    * Sent when client would like to disconnect from the service.
    ***********************************************************************/
	public static final int MSG_DEREGISTER_CLIENT = 2;
	
	/***********************************************************************
    * Used by the client to request connection to a specific Bluetooth LE
    * device.
    ***********************************************************************/
	public static final int MSG_CONNECT_BTLE_DEVICE= 3;
	
	/***********************************************************************
    * Used by the client to request disconnection from a specific
    * Bluetooth LE device.
    ***********************************************************************/
	public static final int MSG_DISCONNECT_BTLE_DEVICE = 4;
	
	/***********************************************************************
    * Used by the client to specify that it would like to start receiving
    * notifications of sensor data updates and to begin logging of that
    * data to the Workout database.
    ***********************************************************************/
	public static final int MSG_BTLE_DEVICE_START_SESSION = 5;
	
	/***********************************************************************
    * Sent when the client would like to stop the stream of updates and
    * cease adding to the database.
    ***********************************************************************/
	public static final int MSG_BTLE_DEVICE_STOP_SESSION = 6;
	
	/***********************************************************************
    * Sent when the client would like to pause data acquisition
    * temporarily; database updates should be halted but we still send
    * data updates to the client.
    ***********************************************************************/
	public static final int MSG_BTLE_DEVICE_PAUSE_SESSION = 7;
	
	/***********************************************************************
    * Sent when the client would like to resume updates to the database
    * using the same session ID.
    ***********************************************************************/
	public static final int MSG_BTLE_DEVICE_RESUME_SESSION = 8;
	
	/***********************************************************************
    * Sent when the client would like to change the session ID in use.
    ***********************************************************************/
	public static final int MSG_BTLE_DEVICE_MODIFY_SESSION = 9;
	
	public static final int MSG_BTLE_START_CLIENT_DATA_UPDATES = 10;
	public static final int MSG_BTLE_STOP_CLIENT_DATA_UPDATES = 11;
	
	/***********************************************************************
    * Sent when the client activity is being destroyed and it would like
    * to force disconnection of all Bluetooth LE devices and shutdown
    * the service.
    ***********************************************************************/
	public static final int MSG_SHUTDOWN_SERVER = 100;
	
	/***********************************************************************
     * List of message types that can be sent to registered clients.
     ***********************************************************************/
	public static final int MSG_REGISTRATION_ACCEPTED = 1;
	public static final int MSG_REGISTRATION_DENIED = 2;
	public static final int MSG_DEREGISTER_ACK = 3;
	public static final int MSG_BTLE_CONNECT_SUCCESS = 4;
	public static final int MSG_BTLE_CONNECT_FAILED = 5;
	public static final int MSG_BTLE_CONNECTION_DROPPED = 6;
	public static final int MSG_BTLE_DATA_UPDATE = 7;
	public static final int MSG_BTLE_DEVICE_START_SESSION_ACK = 8;
	public static final int MSG_BTLE_DEVICE_STOP_SESSION_ACK = 9;
	public static final int MSG_BTLE_DEVICE_PAUSE_SESSION_ACK = 10;
	public static final int MSG_BTLE_DEVICE_RESUME_SESSION_ACK = 11;
	public static final int MSG_BTLE_DEVICE_INCREMENT_LAP_SESSION_ACK = 12;
	public static final int MSG_DISPLAY_TIME_UPDATE = 13;
	public static final int MSG_RESET_TIME_AND_DISTANCE = 14;
	public static final int MSG_BTLE_DEVICE_NOT_FOUND = 15;
	
	////////////////////////////////////////////////////////////////////////
	//  Globals managed by the IncomingHandler.
	////////////////////////////////////////////////////////////////////////
	private static final int DEFAULT_SESSION_ID = -1;
	private double mCscWheelCircumference;
	private int mSessionId;
	
	
	/********************************************************************************
     * Target we publish for clients to send messages to IncomingHandler.
     ********************************************************************************/
    private final Messenger mMessenger = new Messenger(new IncomingHandler());
	
	/***********************************************************************
     * Manages messages sent from the registered clients.
     ***********************************************************************/
	public class IncomingHandler extends Handler {
		@Override
		public void handleMessage(Message msg) {
			Messenger messenger = msg.replyTo;
			Bundle data = (Bundle) msg.getData();
			
			switch(msg.what) {
			case MSG_REGISTER_CLIENT:
				
				synchronized(mClientSyncKey) {
					mClient = messenger;
				}
				
				sendMsgToClient(MSG_REGISTRATION_ACCEPTED, 0, 0, null);
				break;
				
			case MSG_DEREGISTER_CLIENT:
				sendMsgToClient(MSG_DEREGISTER_ACK, 0, 0, null);
				clientConnectionLost();
				break;
			
			default:
				handleClientMessage(msg.what, data);
			}
			
			
		}
	}
	
	private void handleClientMessage(int event, Bundle data) {
		
		String address;
		BtleDevice btleDevice;
		Log.d(TAG, "(Service - handleMessage) " + event);
		
		switch(event) {
			
		case MSG_CONNECT_BTLE_DEVICE:
			address = data.getString(EXTRA_BTLE_ADDRESS);
			BtleDeviceType devType = BtleDeviceType.fromInt(data.getInt(EXTRA_BTLE_TYPE));
			
			
			if((btleDevice = findDeviceByAddress(address)) == null) {
				// Create a new device and try to connect.
				btleDevice = new BtleDevice(
					address, devType, mDeviceCallback, getApplicationContext());
				btleDevice.initiateDeviceConnection();
			} else {
				// Device already exists, so send a message to the client that we're ready.
				Bundle bundle = new Bundle();
				bundle.putString(EXTRA_BTLE_ADDRESS, btleDevice.getBtleDeviceAddress());
				bundle.putString(EXTRA_BTLE_DEVICE_NAME, btleDevice.getDeviceName());
				sendMsgToClient(MSG_BTLE_CONNECT_SUCCESS, 0, 0, bundle);
			}
			break;
			
		case MSG_DISCONNECT_BTLE_DEVICE:
			address = data.getString(EXTRA_BTLE_ADDRESS);
			btleDevice = findDeviceByAddress(address);
			if(btleDevice != null) {
				mClientDevices.remove(btleDevice);
				btleDevice.disableSensorDataUpload();
				btleDevice.closeDeviceConnection();
			}
			else {
				// We don't have that device. Tell the client that it's gone already.
				Bundle bundle = new Bundle();
				BtleDeviceStatus status = BtleDeviceStatus.BTLE_DEVICE_DOES_NOT_EXIST;
				bundle.putString(EXTRA_BTLE_ADDRESS, address);
				bundle.putString(EXTRA_BTLE_ERROR_STRING, status.toString());
				bundle.putInt(EXTRA_BTLE_ERROR_VALUE, status.getValue());
				sendMsgToClient(MSG_BTLE_DEVICE_NOT_FOUND, 0, 0, bundle);
			}
			break;
			
		case MSG_BTLE_DEVICE_START_SESSION:
			mSessionId = data.getInt(EXTRA_SESSION_ID);
			showServiceNotification();
			enableDbUpdates(mSessionId);
			sendMsgToClient(MSG_BTLE_DEVICE_START_SESSION_ACK, mSessionId, 0, null);
			vocalizeString("Workout started");
			startSessionTimer();
			break;
			
		case MSG_BTLE_DEVICE_STOP_SESSION:
			
			disableDbUpdates();
			vocalizeString("Workout stopped");
			clearServiceNotification();
			sWorkoutDb.stopActiveWorkoutSession(getApplicationContext());
			sWorkoutDb.createWorkoutSessionSummary(mSessionId, false);
			sendMsgToClient(MSG_BTLE_DEVICE_STOP_SESSION_ACK, mSessionId, 0, null);
			break;
			
		case MSG_BTLE_DEVICE_PAUSE_SESSION:
			disableDbUpdates();
			sWorkoutDb.pauseActiveWorkoutSession(getApplicationContext());
			vocalizeString("Workout paused");
			sendMsgToClient(MSG_BTLE_DEVICE_PAUSE_SESSION_ACK, mSessionId, 0, null);
			break;
			
		case MSG_BTLE_DEVICE_RESUME_SESSION:
			enableDbUpdates(mSessionId);
			vocalizeString("Workout resumed");
			sWorkoutDb.resumePausedWorkoutSession(getApplicationContext());
			sendMsgToClient(MSG_BTLE_DEVICE_RESUME_SESSION_ACK, mSessionId, 0, null);
			continueSessionTimer();
			break;
			
		case MSG_BTLE_DEVICE_MODIFY_SESSION:
			mSessionId = data.getInt(EXTRA_SESSION_ID);
			enableClientNotifications();
			enableDbUpdates(mSessionId);
			break;
			
		case MSG_BTLE_START_CLIENT_DATA_UPDATES:
			double tempWheelInfo = data.getDouble(EXTRA_WHEEL_CIRCUMFERENCE, 0.0);
			if(tempWheelInfo != 0.0) {
				mCscWheelCircumference = tempWheelInfo;
			}
			
			enableClientNotifications();
			break;
		
		case MSG_BTLE_STOP_CLIENT_DATA_UPDATES:
			disableClientNotifications();
			break;
			
		case MSG_RESET_TIME_AND_DISTANCE:
			resetSessionTimeDistance();
			break;
			
		case MSG_SHUTDOWN_SERVER:
			shutdownServerProcess();
			break;
			
		}
	}
	
	/***********************************************************************
     * List of connected BtleDevice. Note that as soon as we lose
     * connectivity to a BtleDevice we cleanup its connection to the
     * BluetoothManager and throw the object away.
     ***********************************************************************/
	private ArrayList<BtleDevice> mClientDevices;
	
	
	/***********************************************************************
     * Search the mClientDevices list for a BtleDevice that is assigned
     * the address passed by the caller. Return that device, or null
     * if none is found.
     ***********************************************************************/
	private BtleDevice findDeviceByAddress(String address) {
		for(BtleDevice s : mClientDevices) {
			if(s.getBtleDeviceAddress().equals(address))
				return s;
		}
		
		return null;
	}
	
	/***********************************************************************
     * Callback definitions required by BtleDevice
     ***********************************************************************/
	private BtleDevice.BtleDeviceCallback mDeviceCallback = new BtleDevice.BtleDeviceCallback() {

		@Override
		public void transferCharacteristicData_heartRate(BtleDevice device,
				long timestamp, int sessionId, int bpm) {
			
			if(clientNotificationsActive()) {
				Bundle bundle = translateHrmValues(timestamp, bpm);
				bundle.putString(EXTRA_BTLE_ADDRESS, device.getBtleDeviceAddress());
				sendMsgToClient(MSG_BTLE_DATA_UPDATE,
						BtleDeviceType.HEART_RATE_MONITOR.getValue(), 0, bundle);
			}
			
			if(dbUpdatesActive()) {
				sWorkoutDb.insertTable_heartRate(timestamp, sessionId, bpm);
			}
			
		}

		@Override
		public void transferCharacteristicData_cyclePower(BtleDevice device,
				long timestamp,
				int sessionId, int instantPower, double pedalPowerBalance,
				double accumulatedTorque, int cumWheelRevs, int cumCrankRevs,
				int lastWheelTime, int lastCrankTime,
				int maxForce, int minForce, double maxTorque, double minTorque,
				int maxAngle, int minAngle, int topDeadSpotAngle,
				int bottomDeadSpotAngle, int accumulatedEnergy) {
			
			if(clientNotificationsActive()) {
				Bundle bundle = translateCyclePowerValues(
						timestamp, sessionId, instantPower, pedalPowerBalance,
						accumulatedTorque, cumWheelRevs, cumCrankRevs,
						lastWheelTime, lastCrankTime,
						maxForce, minForce, maxTorque, minTorque,
						maxAngle, minAngle, topDeadSpotAngle,
						bottomDeadSpotAngle, accumulatedEnergy);
				bundle.putString(EXTRA_BTLE_ADDRESS, device.getBtleDeviceAddress());
				sendMsgToClient(MSG_BTLE_DATA_UPDATE,
						BtleDeviceType.CYCLING_POWER_METER.getValue(), 0, bundle);
				
			}
			
			if(dbUpdatesActive()) {
				sWorkoutDb.insertTable_cyclePower(
						timestamp, sessionId, instantPower, pedalPowerBalance, 
						accumulatedTorque, cumWheelRevs, cumCrankRevs,
						lastWheelTime, lastCrankTime, maxForce, 
						minForce, maxTorque, minTorque, maxAngle, minAngle, 
						topDeadSpotAngle, bottomDeadSpotAngle, accumulatedEnergy);
			}
			
		}

		@Override
		public void transferCharacteristicData_cycleSpeedCadence(BtleDevice device, long timestamp,
				int sessionId, int cumWheelRevs, int cumCrankRevs, int lastWheelTime, int lastCrankTime) {

			if(clientNotificationsActive()) {
				Bundle bundle = translateCscValues(timestamp, cumWheelRevs, cumCrankRevs, lastWheelTime, lastCrankTime);
				bundle.putString(EXTRA_BTLE_ADDRESS, device.getBtleDeviceAddress());
				sendMsgToClient(MSG_BTLE_DATA_UPDATE,
						BtleDeviceType.CYCLING_SPEED_CADENCE_SENSOR.getValue(), 0, bundle);
			}
			
			if(dbUpdatesActive()){
				sWorkoutDb.insertTable_cycleSpeedCadence(timestamp, sessionId, cumWheelRevs, cumCrankRevs, lastWheelTime, lastCrankTime);
			}
			
		}

		@Override
		public void onSuccessfulDeviceConnection(BtleDevice btleDevice) {
			
			mClientDevices.add(btleDevice);
			
			if(clientNotificationsActive()) {
				btleDevice.setReferenceId(mSessionId);
			}
			
			if(btleDevice.getBtleDeviceType().equals(BtleDeviceType.CYCLING_SPEED_CADENCE_SENSOR)) {
				initializeCscGlobals();
			} else if (btleDevice.getBtleDeviceType().equals(BtleDeviceType.CYCLING_POWER_METER)) {
				initializePowerGlobals();
			}
			
			// Inform the registered client that this BtleDevice was successfully connected
			// and is ready to transfer sensor data.
			Bundle bundle = new Bundle();
			bundle.putString(EXTRA_BTLE_ADDRESS, btleDevice.getBtleDeviceAddress());
			bundle.putString(EXTRA_BTLE_DEVICE_NAME, btleDevice.getDeviceName());
			sendMsgToClient(MSG_BTLE_CONNECT_SUCCESS, 0, 0, bundle);
		}

		@Override
		public void onFailedDeviceConnection(final BtleDevice btleDevice, final BtleDeviceStatus status) {
			
			// Inform client of status and let it determine how to proceed.
			Bundle bundle = new Bundle();
			bundle.putString(EXTRA_BTLE_ADDRESS, btleDevice.getBtleDeviceAddress());
			bundle.putString(EXTRA_BTLE_ERROR_STRING, status.toString());
			bundle.putInt(EXTRA_BTLE_ERROR_VALUE, status.getValue());
			sendMsgToClient(MSG_BTLE_CONNECT_FAILED, 0, 0, bundle);
			
			// Device is not being used so close the connection if still active and remove the
			// BtleDevice object from mClientDevices.
			btleDevice.closeDeviceConnection();
			mClientDevices.remove(btleDevice);
		}

		@Override
		public void onDeviceConnectionDropped(final BtleDevice btleDevice, final BtleDeviceStatus status) {
			// Inform client of status and let it determine how to proceed.
			Bundle bundle = new Bundle();
			bundle.putString(EXTRA_BTLE_ADDRESS, btleDevice.getBtleDeviceAddress());
			bundle.putString(EXTRA_BTLE_ERROR_STRING, status.toString());
			bundle.putInt(EXTRA_BTLE_ERROR_VALUE, status.getValue());
			sendMsgToClient(MSG_BTLE_CONNECTION_DROPPED, 0, 0, bundle);
			
			// Device is not being used so close the connection if still active and remove the
			// BtleDevice object from mClientDevices.
			btleDevice.closeDeviceConnection();
			mClientDevices.remove(btleDevice);
		}

	};

    
    /********************************************************************************
     * Handles start/stop of device characteristic notifications.
     ********************************************************************************/
    private boolean mClientNotificationsActive;
    
    /********************************************************************************
     * Handles start/stop of database updates.
     ********************************************************************************/
    private boolean mDatabaseUpdatesActive;
    
    /********************************************************************************
     * Handles start/stop of device characteristic notifications.
     ********************************************************************************/
    private boolean mCharacteristicNotificationsActive;
    
    public boolean characteristicNotificationsActive() {
    	synchronized(mCharacteristicNotificationsActiveSyncKey) {
    		return mCharacteristicNotificationsActive;
    	}
    }
    
    private void enableCharacteristicNotifications(int sessionId) {
    	synchronized(mCharacteristicNotificationsActiveSyncKey) {
    		mCharacteristicNotificationsActive = true;
    	}
    	
    	// Call notification start on all devices.
		for(BtleDevice s : mClientDevices) {
			s.enableSensorDataUpload(sessionId);
		}
    }
    
    private void disableCharacteristicNotifications() {
	    synchronized(mCharacteristicNotificationsActiveSyncKey) {
			mCharacteristicNotificationsActive = false;
		}
	    
	    // Call notification stop on all devices.
 		for(BtleDevice s : mClientDevices) {
 			s.disableSensorDataUpload();
 		}
    }
    
    /********************************************************************************
	 * Key values used for thread synchronization.
	 ********************************************************************************/
	private Object mDatabaseUpdatesActiveSyncKey = new Object();
	private Object mNotificationsActiveSyncKey = new Object();
	private Object mClientSyncKey = new Object();
	private Object mCharacteristicNotificationsActiveSyncKey = new Object();
    
	/********************************************************************************
	 * Check if data update notifications have been requested by the client.
	 * @return	boolean
	 * 	true if notifications have been enabled, false otherwise.
	 ********************************************************************************/
    public boolean clientNotificationsActive() { 
    	synchronized(mNotificationsActiveSyncKey) {
    		return mClientNotificationsActive; 
    	}
    }
    
    /********************************************************************************
	 * Inform each BtleDevice that we no longer want updated sensor data, and
	 * mark off that we have done this.
	 ********************************************************************************/
    private void disableClientNotifications() {
    	synchronized(mNotificationsActiveSyncKey) {
    		mClientNotificationsActive = false;
    	}
    	
    	if(!dbUpdatesActive()) {
    		disableCharacteristicNotifications();
    	}
    	
    }
    
    /********************************************************************************
	 * Inform each BtleDevice that we want to be updated with future sensor data,
	 * mark off that we have done this.
	 ********************************************************************************/
    private void enableClientNotifications() {
    	synchronized(mNotificationsActiveSyncKey) {
    		mClientNotificationsActive = true;
    	}
    	
    	enableCharacteristicNotifications(DEFAULT_SESSION_ID);

    }
    
    /********************************************************************************
	 * Set boolean so that database updates are now blocked.
	 ********************************************************************************/
    private void enableDbUpdates(int sessionId) {
    	synchronized(mDatabaseUpdatesActiveSyncKey) {
    		mDatabaseUpdatesActive = true;
    	}
    	
    	enableCharacteristicNotifications(sessionId);
    }
    
    /********************************************************************************
	 * Set boolean so that database updates are allowed.
	 ********************************************************************************/
    private void disableDbUpdates() {
    	synchronized(mDatabaseUpdatesActiveSyncKey) {
    		mDatabaseUpdatesActive = false;
    	}
    	
    	if(!clientNotificationsActive()) {
    		disableCharacteristicNotifications();
    	}
    }
    
    /********************************************************************************
	 * Check if database updates have been requested by the client.
	 * @return	boolean
	 * 	true if updates have been enabled, false otherwise.
	 ********************************************************************************/
    public boolean dbUpdatesActive() {
    	synchronized(mDatabaseUpdatesActiveSyncKey) {
    		return mDatabaseUpdatesActive;
    	}
    }
	
	private ArrayList<Integer> mWheelRevs;
	private ArrayList<Integer> mWheelTime;
	private ArrayList<Integer> mCrankRevs;
	private ArrayList<Integer> mCrankTime;
	
	private final int ROLLING_AVG_COUNT = 6;
	private final int ROLLING_AVG_COUNT_WHEEL = 4;
	private final int UNSIGNED_INT16_MAX_VALUE = 65536;
	private final double CSC_TIME_RESOLUTION = 1024.0;
	
	private void initializeRollingAvgLists() {
		mWheelRevs = new ArrayList<Integer>();
		mWheelTime = new ArrayList<Integer>();
		mCrankRevs = new ArrayList<Integer>();
		mCrankTime = new ArrayList<Integer>();
	}
	
	private double updateWheelRollingAvg(int rev, int time) {
		double sumRev = 0;
		double sumTime = 0;
		
		if(mWheelRevs.size() < ROLLING_AVG_COUNT_WHEEL) {
			mWheelRevs.add(rev);
			mWheelTime.add(time);
		} else {
			mWheelRevs.remove(0);
			mWheelTime.remove(0);
			mWheelRevs.add(rev);
			mWheelTime.add(time);
		}
		
		StringBuilder wheelRevInfo = new StringBuilder();
		StringBuilder wheelTimeInfo = new StringBuilder();
		
		
		if(mWheelRevs.size() <= 1) {
			return 0.0;
		} else {
			// Calculate a sum of the revolution/time differences in the list
			for(int i = 1; i < mWheelRevs.size(); i++) {
				if(mWheelRevs.get(i) >= mWheelRevs.get(i - 1)) {
					sumRev += (mWheelRevs.get(i) - mWheelRevs.get(i - 1));
					wheelRevInfo.append(String.format("%d ", mWheelRevs.get(i) - mWheelRevs.get(i - 1)));
				} else {
					// Wheel revs is stored as unsigned 32-bit integer on device,
					// so this indicates the value has rolled over.
					// TODO need to fix this in the future to handle values over 2^31
					
				}
				
				if(mWheelTime.get(i) >= mWheelTime.get(i - 1)) {
					sumTime += (mWheelTime.get(i) - mWheelTime.get(i - 1));
					wheelTimeInfo.append(String.format("%d ", mWheelTime.get(i) - mWheelTime.get(i - 1)));
				} else {
					// Time is stored as an unsigned 16-bit short on device,
					// so this indicates the value has rolled over.
					sumTime += (mWheelTime.get(i) + UNSIGNED_INT16_MAX_VALUE - mWheelTime.get(i - 1));
					wheelTimeInfo.append(String.format("%d ", mWheelTime.get(i) + UNSIGNED_INT16_MAX_VALUE - mWheelTime.get(i - 1)));
				}
			}
		}
		
		//Log.d("updateWheelRollingAvg", "sumRev: " + sumRev + " " + wheelRevInfo.toString());		
		//Log.d("updateWheelRollingAvg", "sumTime: " + sumTime + " " + wheelTimeInfo.toString());
		//Log.d("updateWheelRollingAvg", "rollingAvgSpeed: " + String.format("%1.2f", (sumRev * 1024 / sumTime * WHEEL_CIRCUMFERENCE)));
		
		if(sumTime > 0) {
			return (sumRev * 1024 / sumTime * mCscWheelCircumference);
		} else {
			return 0;
		}
	}
	
	private double updateCrankRollingAvg(int rev, int time) {
		double sumRev = 0;
		double sumTime = 0;
		
		if(mCrankRevs.size() < ROLLING_AVG_COUNT) {
			mCrankRevs.add(rev);
			mCrankTime.add(time);
		} else {
			mCrankRevs.remove(0);
			mCrankTime.remove(0);
			mCrankRevs.add(rev);
			mCrankTime.add(time);
		}
		
		
		if(mCrankRevs.size() <= 1) {
			return 0.0;
		} else {
			// Calculate a sum of the revolution/time differences in the list
			for(int i = 1; i < mCrankRevs.size(); i++) {
				if(mCrankRevs.get(i) >= mCrankRevs.get(i-1)) {
					sumRev += (mCrankRevs.get(i) - mCrankRevs.get(i - 1));
				} else {
					// Crank revs is an unsigned 16-bit short on device, so
					// this indicates the value has rolled over.
					sumTime += (mCrankRevs.get(i) + UNSIGNED_INT16_MAX_VALUE - mCrankRevs.get(i - 1));
				}
				
				if(mCrankTime.get(i) >= mCrankTime.get(i - 1)) {
					sumTime += (mCrankTime.get(i) - mCrankTime.get(i - 1));
				} else {
					// Time is stored as an unsigned 16-bit short on device,
					// so this indicates the value has rolled over.
					sumTime += (mCrankTime.get(i) + UNSIGNED_INT16_MAX_VALUE - mCrankTime.get(i - 1));
				}
				
			}
		}
		
		if(sumTime > 0) {
			return sumRev * 1024 / sumTime;
		} else {
			return 0;
		}
	}
	
	private void initializePowerGlobals() {
		
	}
	
	/********************************************************************************
     * Converts base values sent by Cycle Power Meter BtleDevice into watts, KPH and
     * RPM and packs them into a bundle.
     ********************************************************************************/
	private Bundle translateCyclePowerValues(long timestamp,
			int sessionId, int instantPower, double pedalPowerBalance,
			double accumulatedTorque, int cumWheelRevs, int cumCrankRevs,
			int lastWheelTime, int lastCrankTime,
			int maxForce, int minForce, double maxTorque, double minTorque,
			int maxAngle, int minAngle, int topDeadSpotAngle,
			int bottomDeadSpotAngle, int accumulatedEnergy) {
		Bundle bundle = new Bundle();
		bundle.putInt(EXTRA_POWER_WATTS, instantPower);
		return bundle;
		
	}
	
	/********************************************************************************
     * Nothing to translate, so just creates a bundle containing beats per minute
     * value.
     ********************************************************************************/
	private Bundle translateHrmValues(long timestamp, int bpm) {
		Bundle bundle = new Bundle();
		bundle.putInt(EXTRA_HRM_BPM, bpm);
		bundle.putLong(EXTRA_TIMESTAMP, timestamp);
		return bundle;
	}
	
	private int mCscLastWheelRevs;
	private int mCscLastCrankRevs;
	private long mCscLastTimestamp;
	private int mCscLastWheelTime;
	private int mCscLastCrankTime;
	
	/********************************************************************************
     * Set all CSC global values to -1.
     ********************************************************************************/
	private void initializeCscGlobals() {
		mCscLastWheelRevs = -1;
		mCscLastCrankRevs = -1;
		mCscLastTimestamp = -1;
		mCscLastWheelTime = -1;
		mCscLastCrankTime = -1;
		initializeRollingAvgLists();
	}
	
	/********************************************************************************
     * Converts base values sent by BtleDevice into KPH and RPM and packs them
     * into a bundle.
     ********************************************************************************/
	private Bundle translateCscValues(
			long timestamp, final int cumWheelRevs, final int cumCrankRevs,
				final int wheelTime, final int crankTime) {
		
		Bundle bundle = new Bundle();
		double wheelRollingAvg = updateWheelRollingAvg(cumWheelRevs, wheelTime);
		double crankRollingAvg = updateCrankRollingAvg(cumCrankRevs, crankTime);
		
		if(mCscLastWheelRevs < 0) {
			mCscLastWheelRevs = cumWheelRevs;
			mCscLastCrankRevs = cumCrankRevs;
			mCscLastWheelTime = wheelTime;
			mCscLastCrankTime = crankTime;
			mCscLastTimestamp = timestamp;
			
			// Fill the bundle and return
			bundle.putDouble(EXTRA_CSC_SPEED, 0);
			bundle.putInt(EXTRA_CSC_CRANK_RPM, 0);
			bundle.putLong(EXTRA_TIMESTAMP, timestamp);
			return bundle;
		}
		
		double crankTimeElapsed;
		double wheelTimeElapsed;
		double speedInMetersPerSec;
		double cranksPerSec;
		double distanceTraveled;
		
		
		if(wheelTime == mCscLastWheelTime) {
			// no data received
			wheelTimeElapsed = 0;
			speedInMetersPerSec = 0;
			distanceTraveled = 0;
			
		} else {
			if(wheelTime > mCscLastWheelTime) {
				wheelTimeElapsed = ((double)(wheelTime - mCscLastWheelTime)) / CSC_TIME_RESOLUTION;
			} else {
				wheelTimeElapsed = ((double)(wheelTime + UNSIGNED_INT16_MAX_VALUE - mCscLastWheelTime)) / CSC_TIME_RESOLUTION;
			}

			distanceTraveled = ((double)(cumWheelRevs - mCscLastWheelRevs)) * mCscWheelCircumference;
			speedInMetersPerSec = 
					distanceTraveled / wheelTimeElapsed;
		}
		
		if(dbUpdatesActive()) {
			mSessionDistanceTraveled += distanceTraveled;
		}
		
		if(crankTime == mCscLastCrankTime) {
			// no data received
			crankTimeElapsed = 0;
			cranksPerSec = 0;
		}
		else { 
			if(crankTime > mCscLastCrankTime) {
				crankTimeElapsed = ((double)(crankTime - mCscLastCrankTime)) / CSC_TIME_RESOLUTION;
			} else {
				crankTimeElapsed = ((double)(crankTime + UNSIGNED_INT16_MAX_VALUE - mCscLastCrankTime)) / CSC_TIME_RESOLUTION;
			}
			
			cranksPerSec = ((double)(cumCrankRevs - mCscLastCrankRevs)) / crankTimeElapsed;
		}
		
		
		int crankRPM = ((int) Math.ceil(60.0 * cranksPerSec));
		double speedKPH = (speedInMetersPerSec * 3.600);
		
		if(speedKPH == 0.0) {
			// Use the rolling average instead.
			speedKPH = (wheelRollingAvg * 3.600);
		}
		
		if(crankRPM == 0) {
			// Use the rolling average instead
			crankRPM = ((int) Math.ceil(60.0 * crankRollingAvg));
		}
		
		Log.d(TAG, String.format("time: %d ms, speed: %1.2f KPH, cadence: %d RPM",
				timestamp - mCscLastTimestamp, speedKPH, crankRPM));
		
		// Update the "last" value trackers with the current values.
		mCscLastWheelRevs = cumWheelRevs;
		mCscLastCrankRevs = cumCrankRevs;
		mCscLastWheelTime = wheelTime;
		mCscLastCrankTime = crankTime;
		mCscLastTimestamp = timestamp;
		
		// Fill the bundle and return
		bundle.putDouble(EXTRA_CSC_SPEED, speedKPH);
		bundle.putInt(EXTRA_CSC_CRANK_RPM, crankRPM);
		bundle.putLong(EXTRA_TIMESTAMP, timestamp);
		bundle.putDouble(EXTRA_TOTAL_DISTANCE, mSessionDistanceTraveled);
		return bundle;
	}
	
	////////////////////////////////////////////////////////////////////////
	//  JSON save and retrieve functionality for persistent storage of
    //	BtleDevice objects when service is shutdown unexpectedly.
	////////////////////////////////////////////////////////////////////////
    private static final String FILENAME = "CycleCoach_btleDevices.json";
	
	/***********************************************************************
     * Used to stash all of the active devices when Android kill the service
     * to free memory.
     ***********************************************************************/
	public void saveDevices() 
		throws JSONException, IOException {
		
		// Build an array in JSON
		JSONArray array = new JSONArray();
		for(BtleDevice d : mClientDevices)
			array.put(d.toJSON());
		
		// Write the file to disk
		Writer writer = null;
		try {
			OutputStream out = getApplicationContext().openFileOutput(
					FILENAME, Context.MODE_PRIVATE);
			writer = new OutputStreamWriter(out);
			writer.write(array.toString());
			
		} finally {
			if (writer != null) writer.close();
		}
	}
	
	/***********************************************************************
     * Retrieve settings for all formerly active devices and recreate
     * BtleDevice objects.
     ***********************************************************************/
	public ArrayList<BtleDevice> loadDevices()
		throws IOException, JSONException {
		ArrayList<BtleDevice> devices = new ArrayList<BtleDevice>();
		BufferedReader reader = null;
		try {
			// open and read the file into a StringBuilder
			InputStream in = getApplicationContext().openFileInput(FILENAME);
			reader = new BufferedReader(new InputStreamReader(in));
			StringBuilder jsonString = new StringBuilder();
			String line = null;
			while((line = reader.readLine()) != null) {
				// line breaks are omitted and irrelevant
				jsonString.append(line);
			}
			JSONArray array = (JSONArray) new JSONTokener(jsonString.toString()).nextValue();
			// Build the array of BtleDevice from JSONObjects
			for(int i = 0; i < array.length(); i++) {
				mClientDevices.add(new BtleDevice(array.getJSONObject(i), mDeviceCallback, getApplicationContext()));
			}
		} catch (FileNotFoundException e) {
			// ignore
		} finally {
			if (reader != null) {
				reader.close();
				
				// delete the JSON file
				getApplicationContext().deleteFile(FILENAME);
			}
		}
		
		return devices;
	}
}
